import os
import re
import time
from typing import Union, List

from scripts.Log import Log
from scripts.config.Config import Config
from scripts.device.AdbShell import AdbShell
from scripts.device.Device import Device


class AdbDevice(Device):
    ADB_BIN = os.path.join('bin', 'adb.exe')
    _BIN: Union[None, str] = None

    DUMP_EXEC = Config.ADB_DEVICE_DUMP_EXEC

    PATTERN_ADB_DAEMON_START = re.compile(r'\s*\* daemon not running;.*')
    PATTERN_ADB_DAEMON_STARTED = re.compile(r'\s*\* daemon started successfully.*')

    def __init__(self, sn: str):
        self._sn = sn

    @staticmethod
    def bin() -> str:
        if AdbDevice._BIN is None:
            AdbDevice._BIN = os.path.join(Device.script_dir(), AdbDevice.ADB_BIN)
        return AdbDevice._BIN

    def sn(self):
        return self._sn

    @staticmethod
    def ignore_adb_daemon_start_info(lines: List[str]) -> List[str]:
        if len(lines) >= 2 and\
                AdbDevice.PATTERN_ADB_DAEMON_START.match(lines[0]) and\
                AdbDevice.PATTERN_ADB_DAEMON_STARTED.match(lines[1]):
            return lines[2:]
        else:
            return lines

    @staticmethod
    def _cmd_shell(cmd: str, retry: int = 3) -> (int, List[str]):
        _, stdout, proc = Device.async_shell('"%s" %s' % (AdbDevice.bin(), cmd))
        code = proc.wait()
        lines = AdbDevice.ignore_adb_daemon_start_info(stdout.readlines())
        while code != 0 and retry != 0:
            Log.w('AdbDevice._cmd_shell()# cmd [%s] failed. retry after 1 second' % cmd)
            Log.w('AdbDevice._cmd_shell()# code: %d, lines: %s' % (code, lines))
            retry -= 1
            time.sleep(1)
            _, stdout, proc = Device.async_shell('"%s" %s' % (AdbDevice.bin(), cmd))
            code = proc.wait()
            lines = AdbDevice.ignore_adb_daemon_start_info(stdout.readlines())
        return code, lines

    @staticmethod
    def static_exec(cmd: str, retry: int = 3) -> (int, List[str]):
        code, lines = AdbDevice._cmd_shell(cmd, retry=retry)

        # check code
        if code != 0:
            Log.e('AdbDevice.static_exec()# failed to execute cmd [%s]' % cmd)
            Log.e('AdbDevice.static_exec()# code: %d, lines: %s' % (code, lines))

        return code, lines

    def exec(self, cmd: str, retry: int = 3) -> (int, List[str]):
        code, lines = AdbDevice._cmd_shell('-s %s %s' % (self.sn(), cmd), retry=retry)

        # check code
        if code != 0:
            Log.e('AdbDevice.exec()# failed to execute cmd [%s]' % cmd)
            Log.e('AdbDevice.exec()# code: %d, lines: %s' % (code, lines))

        return code, lines

    @staticmethod
    def kill_server():
        AdbDevice.static_exec('kill-server')

    @staticmethod
    def devices() -> List[str]:
        _, lines = AdbDevice.static_exec('devices')
        lines = list(filter(lambda l: l.strip() != '', lines[1:]))
        return [line.split()[0] for line in lines]

    @staticmethod
    def wait_for_device(sn: Union[None, str] = None):
        while True:
            AdbDevice.static_exec('wait-for-device')
            devices = AdbDevice.devices()
            if len(devices) > 0:
                if sn is None:
                    return AdbDevice(devices[0])
                elif sn in devices:
                    return AdbDevice(sn)

    def wait_for_device_offline(self, wait_seconds: int = -1) -> bool:
        while wait_seconds != 0:
            wait_seconds -= 1
            devices = AdbDevice.devices()
            if self.sn() not in devices:
                return True
            Log.d('wait for device offline retry after 1 second')
            time.sleep(1)
        return False

    def reboot_bootloader(self):
        self.exec('reboot bootloader', retry=0)

    def reboot(self):
        self.exec('reboot', retry=0)

    def safe_reboot(self, shell=None):
        if shell is None:
            shell = self.shell()

        with shell:
            while True:
                shell.safe_reboot_no_wait()
                if shell.wait_for_shell_exit(30):
                    time.sleep(3)  # sleeep another 3 seconds to ensure device rebooted
                    Log.d('device offline after safe reboot cmd sent')
                    break
                else:
                    Log.w('wait for device offline timeout, resend reboot cmd')

    def adb_root(self):
        self.exec('root', retry=0)

    def pull(self, remote: str, local: str):
        self.exec('pull "%s" "%s"' % (remote, local))

    def push(self, local: str, remote: str):
        self.exec('push "%s" "%s"' % (local, remote))

    def install(self, path: str):
        self.exec('install -r -t "%s"' % path)

    def backup(self, path: str, pkg_list: Union[None, List[str]]):
        pkg_cmd = ' '.join(pkg_list) if pkg_list else '-all'
        self.exec('backup -f "%s" %s' % (path, pkg_cmd))

    def restore(self, path: str):
        self.exec('restore "%s"' % path)

    def enable_verity(self):
        self.exec('enable-verity')

    def disable_verity(self):
        self.exec('disable-verity')

    def shell(self) -> AdbShell:
        return AdbShell(self.sn())
